﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.IO;
using System.Text;

namespace NodaTime.Text
{
    /// <summary>
    ///    A reader that provides fast, non-cached, forward-only access to iCalendar data.
    /// </summary>
    /// <remarks>
    ///   <b>InternetCalendarReader</b> provides forward-only, read-only access to a stream of iCalendar data. The <b>InternetCalendarReader</b> class conforms to the 
    ///   IETF RFC 5455 - Internet Calendaring and Scheduling Core Object Specification
    /// </remarks>
    internal class InternetCalendarReader // TODO: Peek should cope with folded lines.
    {
        static readonly char[] Whitespace = new char[] { ' ', '\t' };

        readonly TextReader reader;

        /// <summary>
        ///   Creates a new instance of the <see cref="InternetCalendarReader"/> with the specified <see cref="TextReader"/>.
        /// </summary>
        /// <param name="reader">
        ///   The <see cref="TextReader"/> from which to read iCalendar data. All iCalendar data is encoded as UTF-8.
        /// </param>
        public InternetCalendarReader(TextReader reader)
        {
            this.reader = reader;
        }

        /// <summary>
        ///   Reads the next character.
        /// </summary>
        /// <returns>
        ///   The next character in input stream.
        /// </returns>
        /// <remarks>
        ///   <b>Read</b> processes a "folded-line", by consuming the CRLF and WHITESPACE and returning the next
        ///   character.
        ///   
        ///   A non-folded CRLF is converted to '\n'. 
        /// </remarks>
        public int Read()
        {
            int c;
            while (true)
            {
                c = reader.Read();
                switch (c)
                {
                    case '\n':
                        if (Whitespace.Contains((char) reader.Peek()))
                        {
                            reader.Read();  // eat WHITESPACE
                            continue;
                        }
                        return c;
                    case '\r':
                        if ('\n' == reader.Peek())
                        {
                            reader.Read();  // eat CRLF
                            if (Whitespace.Contains((char)reader.Peek()))
                            {
                                reader.Read();  // eat WHITESPACE
                                continue;
                            }

                            return '\n';
                        }
                        return c;

                    default:
                        return c;
                }
            }
        }

        /// <summary>
        ///   Reads a name and gaurantees that it matches the <see cref="string"/>.
        /// </summary>
        /// <param name="expectedName">
        ///   The <see cref="string"/> to match.
        /// </param>
        /// <exception cref="FormatException">
        ///   The read name does not match the <paramref name="expectedName"/>.
        /// </exception>
        /// <remarks>
        ///   Matches are case insensitive.
        /// </remarks>
        public void Match(string expectedName)
        {
            var name = ReadName();
            if (name == null)
                throw new FormatException(string.Format("Expected '{0}' not end of stream.", expectedName));
            if (!name.Equals(expectedName, StringComparison.InvariantCultureIgnoreCase))
                throw new FormatException(string.Format("Expected '{0}' not '{1}'.", expectedName, name));
        }

        /// <summary>
        ///   Reads the next character and gaurantees that it matches the specified <see cref="char"/>.
        /// </summary>
        /// <param name="expected">
        ///   The <see cref="char"/> to match.
        /// </param>
        /// <exception cref="FormatException">
        ///   The next character does not match the <paramref name="expected"/> character.
        /// </exception>
        public void Match(char expected)
        {
            var actual = Read();
            if (actual == -1)
                throw new FormatException(string.Format("Expected '{0}' not end of stream.", expected));
            if (actual != expected)
                throw new FormatException(string.Format("Expected '{0}' not '{1}'.", expected, (char) actual));
        }

        /// <summary>
        ///   Reads the next character if it matches the specified <see cref="char"/>.
        /// </summary>
        /// <param name="expected">
        ///   The <see cref="char"/> to match.
        /// </param>
        /// <returns>
        ///   <b>true</b> if the next character equals <paramref name="expected"/>; otherwise, <b>false</b>.
        /// </returns>
        public bool Maybe(char expected)
        {
            if (expected == reader.Peek())
            {
                reader.Read();
                return true;
            }
            return false;
        }

        /// <summary>
        ///   Reads a name.
        /// </summary>
        /// <returns>
        ///   A string or <b>null</b> if at end of stream.
        /// </returns>
        /// <remarks>
        ///   A name is terminated by a ':', '=' or ';'.
        /// </remarks>
        public string ReadName()
        {
            var s = new StringBuilder();
            while (true)
            {
                switch (reader.Peek())
                {
                    case ':':
                    case '=':
                    case ';':
                        return s.ToString();
                    case -1:
                        if (s.Length == 0)
                            return null;
                        return s.ToString();
                }
                s.Append((char)Read());
            }
        }

        /// <summary>
        ///   Reads a parameter value.
        /// </summary>
        /// <remarks>
        ///   A parameter value is terminated by a ':', '=' or ';'. It can be quoted, such as "a value with a colon(:)".
        /// </remarks>
        public string ReadParameterValue()
        {
            if ('"' == reader.Peek())
                return ReadQuotedParameterValue();

            var s = new StringBuilder();
            while (true)
            {
                switch (reader.Peek())
                {
                    case ';':
                    case ':':
                    case ',':
                    case -1:
                        return s.ToString();
                }
                s.Append((char)Read());
            }
        }

        /// <summary>
        ///   Read an enumerable list of parameter values.  Each value separated by a comma (',').
        /// </summary>
        /// <returns></returns>
        public IEnumerable<string> ReadParameterValues()
        {
            do
            {
                yield return ReadParameterValue();
            } while (Maybe(','));
        }

        public IEnumerable<T> ReadParameterValues<T>()
        {
            foreach (var s in ReadParameterValues())
            {
                T value;
                try
                {
                    value = (T)Convert.ChangeType(s, typeof(T), CultureInfo.InvariantCulture);
                }
                catch (Exception e)
                {
                    throw new FormatException(string.Format("Expected a valid {0} value not '{1}'.", typeof(T).Name, s), e);
                }
                yield return value;
            }
        }

        public T ReadParameterValue<T>()
        {
            var value = ReadParameterValue();
            try
            {
                return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
            }
            catch (Exception e)
            {
                throw new FormatException(string.Format("Expected a valid {0} value not '{1}'.", typeof(T).Name, value), e);
            }
        }

        string ReadQuotedParameterValue()
        {
            Read(); // eat the quote
            var s = new StringBuilder();
            while (true)
            {
                char c = (char)Read();
                if (c == '"')
                    break;
                s.Append(c);
            }
            return s.ToString();
        }

    }
}
